package com.lsh.oms.core.service.shipping;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.lsh.base.common.json.JsonUtils;
import com.lsh.base.common.model.CommonResult;
import com.lsh.base.lang.DateTimes;
import com.lsh.base.lang.Maps;
import com.lsh.base.lang.json.Jsons;
import com.lsh.oms.api.model.es.OrderHeadForEs;
import com.lsh.oms.api.model.order.receipt.ReceiptHeadDto;
import com.lsh.oms.api.model.query.QueryShippingDto;
import com.lsh.oms.api.model.shipping.*;
import com.lsh.oms.core.constant.ReturnCode;
import com.lsh.oms.core.dao.other.OrderShippingDetailMapper;
import com.lsh.oms.core.dao.other.OrderShippingHeadMapper;
import com.lsh.oms.core.enums.OrderStatus;
import com.lsh.oms.core.enums.ShippingReceiptStatus;
import com.lsh.oms.core.enums.ShippingStatus;
import com.lsh.oms.core.exception.BusinessException;
import com.lsh.oms.core.model.order.OrderHead;
import com.lsh.oms.core.model.order.OrderShippingDetail;
import com.lsh.oms.core.model.order.OrderShippingHead;
import com.lsh.oms.core.model.order.OrderShippingHeadWithBLOBs;
import com.lsh.oms.core.proxy.service.EsProxy;
import com.lsh.oms.core.proxy.service.TmsProxy;
import com.lsh.oms.core.records.core.ReturnRequest;
import com.lsh.oms.core.records.core.ReturnRequestRepository;
import com.lsh.oms.core.service.order.OrderQueryService;
import com.lsh.oms.core.service.order.OrderReceiptService;
import com.lsh.oms.core.service.order.OrderUpdateService;
import com.lsh.oms.core.service.other.SignOrderService;
import com.lsh.oms.core.service.payment.OrderSignForRefundService;
import com.lsh.oms.core.service.shipping.core.ShippingService;
import com.lsh.oms.core.service.sign.core.SignService;
import com.lsh.oms.core.service.tool.*;
import com.lsh.oms.core.utils.ComputeServiceUtils;
import com.lsh.oms.core.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

import static com.lsh.base.lang.BigDecimals.greaterThanZero;
import static com.lsh.base.lang.BigDecimals.zero;
import static com.lsh.base.lang.bean.Beans.getLong;
import static com.lsh.oms.core.records.core.ReturnRequest.Occasion.BEFORE_SIGN;
import static java.math.BigDecimal.ZERO;

@Component
@Slf4j
public class ShippingServiceImpl implements ShippingService {

    private static Logger LOGGER = LoggerFactory.getLogger(SignOrderService.class);

    @Resource
    private OrderShippingHeadMapper headMapper;

    @Resource
    private OrderShippingDetailMapper detailMapper;

    @Resource
    private OrderQueryService orders;

    @Resource
    private OrderUpdateService orderUpdates;

    @Resource
    private OrderReceiptService receipts;

    @Resource
    private ReturnRequestRepository returnRequests;

    @Resource
    private OrderSignForRefundService refunds;

    @Resource
    private SignService signs;

    @Resource
    private TmsProxy tms;
    
    @Autowired
    private EsProxy esProxy;

    @Value("${assemblyUrl}")
    private String assemblyHost;

    @Override
    public Map<String, Object> findShippingDetailsByOrderId(Long orderId) {
        ensureOrderIdIsValid(orderId);
        Map<String, Object> shippingHead = findShippingHeadByOrderID(orderId);
        Long shippingId = getShippingIdFromHead(shippingHead);
        List<Map<String, Object>> shippingDetail = findShippingOrderDetailsByShippingID(shippingId);
        return merge(shippingHead, shippingDetail);
    }

    private void ensureOrderIdIsValid(Long id) {
        if (id == null || id == 0L) {
            throw new BusinessException(ReturnCode.REQUEST_FAIL, "ORDER ID IS INVALID");
        }
    }

    private Long getShippingIdFromHead(Map<String, Object> head) {
        if (MapUtils.isEmpty(head) || !head.containsKey("shipping_order_id")) {
            throw new BusinessException(ReturnCode.REQUEST_FAIL, "CAN NOT FIND ANY SHIPPING ORDER BY ORDER ID");
        }
        return (Long) head.get("shipping_order_id");
    }

    private Map<String, Object> findShippingHeadByOrderID(Long id) {
        return headMapper.findOrderShippingHeadByOrderId(id);
    }

    private List<Map<String, Object>> findShippingOrderDetailsByShippingID(Long id) {
        List<Map<String, Object>> details = detailMapper.findDetailsByShippingOrderId(id);
        for (Map<String, Object> item : details) {
            JSONObject ext = JSON.parseObject((String) item.get("ext"));

            BigDecimal originPrice = new BigDecimal(ext.getString("origin_price"));
            item.put("origin_price", originPrice);

            BigDecimal sealAmount = originPrice.multiply((BigDecimal) item.get("qty")).setScale(2, BigDecimal.ROUND_HALF_UP);
            item.put("sales_amount", sealAmount);

            BigDecimal eaPrice = originPrice.divide((BigDecimal) item.get("sale_unit"), 2, BigDecimal.ROUND_HALF_UP);
            item.put("ea_price", eaPrice);
        }
        return details;
    }

    private Map<String, Object> merge(Map<String, Object> head, List<Map<String, Object>> details) {
        divideFinalAmountIntoItems(head, details);
        discountAmountOfItems(details);
        enhanceHead(head, details);
        Map<String, Object> result = new HashMap<>();
        result.putAll(head);
        result.put("details", details);
        return result;
    }

    private void enhanceHead(Map<String, Object> head, List<Map<String, Object>> details) {
        BigDecimal salesAmount = ZERO;
        BigDecimal discountAmount = ZERO;
        for (Map<String, Object> item : details) {
            salesAmount = salesAmount.add((BigDecimal) item.get("sales_amount"));
            discountAmount = discountAmount.add((BigDecimal) item.get("discount_amount"));
            item.remove("ext");
        }
        head.put("sales_amount", salesAmount);
        head.put("discount_amount", discountAmount);
    }

    private void divideFinalAmountIntoItems(Map<String, Object> head, List<Map<String, Object>> details) {
        BigDecimal finalAmount = (BigDecimal) head.get("final_amount");
        BigDecimal totalOfItemsSalesAmount = totalOfItemsSalesAmount(details);
        boolean hasNoDiscount = finalAmount.equals(totalOfItemsSalesAmount);
        LinkedList<Map<String, Object>> detailsInQueue = new LinkedList<>(details);
        Map<String, Object> lastOne = detailsInQueue.removeLast();
        for (Map<String, Object> item : detailsInQueue) {
            BigDecimal salesAmount = ((BigDecimal) item.get("sales_amount"));
            BigDecimal finalAmountOfItem = hasNoDiscount
                    ? salesAmount
                    : (salesAmount.divide(totalOfItemsSalesAmount, 2, BigDecimal.ROUND_HALF_UP).multiply(finalAmount).setScale(2, BigDecimal.ROUND_HALF_UP));
            item.put("final_amount", finalAmountOfItem);
        }
        BigDecimal totalOfItemsFinalAmount = totalOfItemsFinalAmount(detailsInQueue);
        lastOne.put("final_amount", finalAmount.subtract(totalOfItemsFinalAmount));
    }

    private void discountAmountOfItems(List<Map<String, Object>> details) {
        for (Map<String, Object> item : details) {
            BigDecimal discountAmount = ((BigDecimal) item.get("sales_amount")).subtract((BigDecimal) item.get("final_amount"));
            item.put("discount_amount", discountAmount);
        }
    }

    private BigDecimal totalOfItemsFinalAmount(List<Map<String, Object>> details) {
        BigDecimal result = ZERO;
        for (Map<String, Object> item : details) {
            result = result.add((BigDecimal) item.get("final_amount"));
        }
        return result;
    }

    private BigDecimal totalOfItemsSalesAmount(List<Map<String, Object>> details) {
        BigDecimal result = ZERO;
        for (Map<String, Object> item : details) {
            result = result.add((BigDecimal) item.get("sales_amount"));
        }
        return result;
    }

    @Override
    public Pageable<OrderShippingHeadWithBLOBs> query(Criteria criteria, long page, long pageLimit) {
        Long count = headMapper.countBaseOnCriteria(criteria);
        List<OrderShippingHeadWithBLOBs> result = count > 0
                ? headMapper.findBaseOnCriteria(criteria, Pageable.start(page, pageLimit), pageLimit)
                : Collections.emptyList();
        fillDetails(criteria, result);
        return new Pageable<>(result, page, pageLimit, count);
    }

    private void fillDetails(Criteria criteria, List<OrderShippingHeadWithBLOBs> heads) {
        if (Criterias.detailsRequired(criteria)) {
            Multimap<Long, OrderShippingDetail> group = group(detailMapper.findDetailList(ids(heads)));
            for (OrderShippingHeadWithBLOBs head : heads) {
                head.setDetails(new ArrayList<>(group.get(head.getShippingOrderId())));
            }
        }
    }

    private Set<Long> ids(List<OrderShippingHeadWithBLOBs> result) {
        return result.stream().map(OrderShippingHead::getShippingOrderId).collect(Collectors.toSet());
    }

    private Multimap<Long, OrderShippingDetail> group(List<OrderShippingDetail> details) {
        Multimap<Long, OrderShippingDetail> group = ArrayListMultimap.create();
        for (OrderShippingDetail item : details) {
            group.put(item.getShippingOrderId(), item);
        }
        return group;
    }

    @Override
    public Optional<OrderShippingHeadWithBLOBs> findByOrderId(Long orderId, boolean detailsRequired) {
        OrderShippingHeadWithBLOBs shipping = headMapper.selectByOrderCode(orderId);
        if (shipping == null) {
            return Optional.empty();
        }
        fillDetails(shipping, detailsRequired);
        return Optional.of(shipping);
    }

    @Override
    public Optional<OrderShippingHeadWithBLOBs> findById(Long id, boolean detailsRequired) {
        OrderShippingHeadWithBLOBs shipping = headMapper.selectByshippingOrderId(id);
        if (shipping == null) {
            return Optional.empty();
        }
        fillDetails(shipping, detailsRequired);
        return Optional.of(shipping);
    }

    @Override
    public Optional<OrderShippingHeadWithBLOBs> findNewByAddressId(Long addressId, boolean detailsRequired) {
        OrderShippingHeadWithBLOBs shipping = headMapper.selectNewByAddressId(addressId);
        if (shipping == null) {
            return Optional.empty();
        }
        fillDetails(shipping, detailsRequired);
        return Optional.of(shipping);
    }

    private void fillDetails(OrderShippingHead shipping, boolean detailsRequired) {
        if (detailsRequired) {
            shipping.setDetails(detailMapper.selectByShippingOrderId(shipping.getShippingOrderId()));
        }
    }

    @Override
    public TmsPageable<OrderShippingHeadWithBLOBs> findByParam(QueryShippingDto filter, boolean detailsRequired) {

        log.info("filter is " + JSON.toJSONString(filter));
        List<OrderShippingHeadWithBLOBs> shippingList = this.headMapper.selectByFilter(filter);
        if (null == shippingList || shippingList.isEmpty()) {
            return new TmsPageable<OrderShippingHeadWithBLOBs>(Collections.EMPTY_LIST, filter.getRn(), 0L);
        }

        Long totalCount = this.headMapper.countByFilter(filter);
        if (detailsRequired) {
            shippingList.forEach(orderShippingHeadWithBLOBs -> orderShippingHeadWithBLOBs.setDetails(detailMapper.selectByShippingOrderId(orderShippingHeadWithBLOBs.getShippingOrderId())));
        }

        return new TmsPageable<>(shippingList, filter.getRn(), totalCount);
    }

    @Override
    public Map<String, Long> findNumByParam(QueryShippingDto filter) {
        Map<String, Long> resmap = new HashMap<>();
        List<Integer> statuses = filter.getStatuses();
        filter.getRoute_ids().forEach((routeId) -> {

            QueryShippingDto query = new QueryShippingDto();

            query.setRoute_ids(Collections.singletonList(routeId));
            query.setStatuses(statuses);
            Long totalCount = this.headMapper.countByFilter(query);

            resmap.put(routeId.toString(), totalCount);
        });


        return resmap;
    }

    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public Boolean updateTrans(ModifyTransDto modifyTransDto) {

        OrderShippingHeadWithBLOBs record = new OrderShippingHeadWithBLOBs();
        record.setRouteId(modifyTransDto.getRoute_id());
        record.setTransUid(modifyTransDto.getTrans_uid());
        record.setUpdatedAt(DateUtil.getCurrentTime());

        return this.headMapper.updateByRouteId(record) > 0;
    }

    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public Boolean updateOmsExt(ModifyShippingExtDto modifyShippingExtDto) {
        OrderShippingHeadWithBLOBs shippingHeadWithBLOBs = headMapper.selectByshippingOrderId(modifyShippingExtDto.getShipping_order_id());
        if (null == shippingHeadWithBLOBs) {
            return false;
        }

        OrderShippingHeadWithBLOBs record = new OrderShippingHeadWithBLOBs();
        record.setShippingOrderId(modifyShippingExtDto.getShipping_order_id());

        JSONObject omsext = JSON.parseObject(shippingHeadWithBLOBs.getOmsExt());
        JSONObject proof_info;
        proof_info = omsext.getJSONObject("proof_info");

        if (null == proof_info) {
            proof_info = new JSONObject();
        }
        JSONArray imgList;
        imgList = proof_info.getJSONArray("img_list");

        if (null == imgList) {
            imgList = new JSONArray();
        }

        imgList.add(modifyShippingExtDto.getProof_id());

        proof_info.put("img_list", imgList);

        omsext.put("proof_info", proof_info);
        omsext.put("proof_time", DateUtil.getCurrentTime());

        record.setOmsExt(omsext.toJSONString());
        record.setUpdatedAt(DateUtil.getCurrentTime());

        return this.headMapper.updateOmsExtByShippingId(record) > 0;
    }


    @Override
    public Boolean setDelay(SetDelayShippingDto setDelayShippingDto) {

        OrderShippingHeadWithBLOBs record = new OrderShippingHeadWithBLOBs();
        record.setShippingOrderId(setDelayShippingDto.getShipping_order_id());
        record.setDelayType(setDelayShippingDto.getDelay_type());
        record.setUpdatedAt(DateUtil.getCurrentTime());

        if (setDelayType(record)) {

            log.info(setDelayShippingDto.getShipping_order_id() + "【ES入参修改发货】-- " + JsonUtils.obj2Json(record));
            esProxy.editIndex(record, setDelayShippingDto.getShipping_order_id() + "");

            return true;
        } else {
            return false;
        }
    }

    @Override
    public List<OrderShippingHeadTmsDto> findByOrderIds(JSONArray orderIds) {

        List<OrderShippingHeadTmsDto> orderList = new ArrayList<>(orderIds.size());
        StopWatch stopWatch = new StopWatch();

        orderIds.forEach(orderId -> {
            Optional<OrderShippingHeadWithBLOBs> orderShippingHeadWithBLOBs = findByOrderId((Long) orderId, true);
//            log.info();
            if (orderShippingHeadWithBLOBs.isPresent()) {
                OrderShippingHeadWithBLOBs orderShippingHead = orderShippingHeadWithBLOBs.get();
                OrderShippingHeadTmsDto headTmsDto = new OrderShippingHeadTmsDto();
                BeanUtils.copyProperties(orderShippingHead, headTmsDto);

                List<OrderShippingDetailTmsDto> tmsDtos = orderShippingHead.getDetails().stream().map(orderShippingDetail -> {
                    OrderShippingDetailTmsDto detailTmsDto = new OrderShippingDetailTmsDto();
                    BeanUtils.copyProperties(orderShippingDetail, detailTmsDto);
                    //特殊处理
//                    detailTmsDto.setCode();
                    return detailTmsDto;
                }).collect(Collectors.toList());
                stopWatch.start();
                JSONArray detailArray = ComputeServiceUtils.getAssemblyResp(String.valueOf(orderShippingHead.getOrderId()), JSONArray.parseArray(JSON.toJSONString(tmsDtos)), "log", assemblyHost + "/cal/index/assembly", orderShippingHead.getVenderId());
                headTmsDto.setItems(detailArray);
                stopWatch.stop();
                log.info("[订单查询及拼装服务耗时] 1 耗时【{}】 ", stopWatch.getTime());
                stopWatch.reset();
                orderList.add(headTmsDto);
            }
        });

//        log.info("[订单查询及拼装服务耗时] 2 耗时【{}】 ", stopWatch.getTime());

        return orderList;
    }

    @Override
    public void update(Long id, OrderShippingHead forUpdate) {
        forUpdate.setShippingOrderId(id);
        headMapper.updateStatusByShippingId(forUpdate);
        Optional.ofNullable(forUpdate.getStatus()).ifPresent($ -> {
            OrderShippingHeadWithBLOBs forEsUpdate = new OrderShippingHeadWithBLOBs();
            forEsUpdate.setShippingOrderId(id);
            forEsUpdate.setStatus($);
            esProxy.editIndex(forEsUpdate, ToolService.getLogSign());
        });
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> receipt(Long id, ReceiptHeadDto request) {
        LOGGER.info("shipping: {} receipt request: {}", id, Jsons.toJsonString(request));
        OrderShippingHeadWithBLOBs shipping = findById(id, true).orElseThrow(BusinessException::shippingNotFound);
        ensureShippingStatusIsAvailable(shipping);
        ensureNotSign(shipping);
        OrderHead order = orders.findById(shipping.getOrderId(), false, true).orElseThrow(BusinessException::orderNotFound);
        ensureOrderStatusIsAvailable(order);
        if (deny(request)) {
            terminateTmsTask(order);
            Long returnId = createReturn(order, shipping, request);
            refreshRelated(order, shipping);
            refunds.orderSignForRefund(order.getOrderCode());
            refunds.orderFinishForCoupon(order.getOrderCode());
            return Maps.of("return", Maps.of("id", returnId));
        } else {
            CommonResult<Object> receiptResponse = receipts.createReceiptOrder(request, ToolService.getLogSign());
            return Maps.of("id", getLong(receiptResponse.getData(), "receiptOrderId"));
        }
    }

    private void ensureNotSign(OrderShippingHeadWithBLOBs shipping) {
        boolean signed = signs.findByOrderId(shipping.getOrderId(), false).isPresent();
        if (signed) {
            throw BusinessException.badRequest("不能重复签收");
        }
    }

    private void terminateTmsTask(OrderHead order) {
        tms.terminateTask(Maps.of("other_id", order.getOrderCode()));
    }

    private void refreshRelated(OrderHead order, OrderShippingHeadWithBLOBs shipping) {
        refreshOrder(order);
        refreshShipping(shipping);
    }

    private void refreshOrder(OrderHead order) {
        orderUpdates.updateOrderStatus(order.getOrderCode(), OrderStatus.ORDER_SIGN);

        OrderHeadForEs forEsUpdate = new OrderHeadForEs();
        forEsUpdate.setOrderCode(order.getOrderCode());
        forEsUpdate.setOrderStatus(OrderStatus.ORDER_SIGN.getIndex());
        esProxy.editIndex(forEsUpdate, ToolService.getLogSign());
    }

    private void refreshShipping(OrderShippingHeadWithBLOBs shipping) {
        OrderShippingHead shippingHead = new OrderShippingHead();
        shippingHead.setStatus(ShippingStatus.DELIVERED.getIndex());
        Integer current = DateTimes.currentSeconds();
        shippingHead.setArrivedAt(current);
        shippingHead.setActivedAt(current);
        shippingHead.setUpdatedAt(current);
        shippingHead.setReceiptStatus(ShippingReceiptStatus.UN_RECEIPT.getIndex());
        update(shipping.getShippingOrderId(), shippingHead);
    }

    private Long createReturn(OrderHead order, OrderShippingHeadWithBLOBs shipping, ReceiptHeadDto request) {
        Set<Integer> weighingGoods = order.getDetails().stream().filter($ -> $.isWeighingGoods()).map($ -> $.getItemCode()).collect(Collectors.toSet());
        List<Map<?, Object>> unsignedItems = shipping.getDetails().stream().map($ -> {
            BigDecimal quantity = weighingGoods.contains($.getItemCode()) ? $.getRealQty() : $.getQty();
            if (greaterThanZero(quantity)) {
                return Maps.of(
                        "item_code", $.getItemCode(),
                        "quantity", quantity,
                        "reason", Maps.of("code", "SHORTCUT", "notes", "", "images", Collections.emptyList()),
                        "opinion", Maps.of("approved", true, "discarded", false, "notes", "")
                );
            } else {
                return Collections.emptyMap();
            }
        }).filter($ -> !$.isEmpty()).collect(Collectors.toList());
        LOGGER.info("unsigned items: {}", Jsons.toJsonString(unsignedItems));
        ReturnRequest created = returnRequests.create(Maps.of("occasion", BEFORE_SIGN.name(), "party", request.getParty(), "order", Maps.newHashMap("id", order.getOrderCode()), "details", unsignedItems, "shipping", Maps.of("deny", true)));
        return getLong(created, "returnId");
    }

    private boolean deny(ReceiptHeadDto request) {
        if (request == null) {
            throw BusinessException.badRequest();
        }
        BigDecimal totalSignQty = Optional.ofNullable(request.getDetails()).orElse(Collections.emptyList()).stream().map($ -> Optional.ofNullable($.getQty()).orElse(ZERO)).reduce(ZERO, BigDecimal::add);
        LOGGER.info("total sign qty: {}", totalSignQty);
        return zero(totalSignQty);
    }

    private void ensureShippingStatusIsAvailable(OrderShippingHeadWithBLOBs shipping) {
        if (!shipping.delivering()) {
            throw BusinessException.badRequest("发货单状态异常");
        }
    }

    private void ensureOrderStatusIsAvailable(OrderHead order) {
        if (!order.delivering()) {
            throw BusinessException.badRequest("订单状态异常");
        }
    }

    @Transactional(rollbackFor = RuntimeException.class)
    public boolean setDelayType(OrderShippingHeadWithBLOBs record) {

        return this.headMapper.updateByShippingOrderId(record) > 0;
    }
}
